﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using Universe.Drawing;
using Universe.MediaProbe.Tests.Videos;

namespace Universe.MediaProbe.Tests
{
    using System.Reflection;
    using System.Text;

    using Utils;

    [TestFixture]
    public class AllTests
    {
        private static readonly int ExpectedWidth = 960;
        private static readonly int ExpectedHeight = 640;

        public AllTests()
        {
            // If ram drive M: isnt present, this config parameter is ignored
            // Environment.SetEnvironmentVariable("SMARTY_TEMP", "M:\\TEMP");
            var tmp = External.ExternalSource.GetFFMpegFile();
        }

        [Test]
        public void Test_0_Sample_Video_Exists()
        {
            Trace.WriteLine("Source video is " + SourceVideo);
            var file = SourceVideo;
            Assert.IsTrue(File.Exists(file));
            long length = new FileInfo(file).Length;
            Assert.Greater(length, 0);
            Trace.WriteLine("Video File: " + length + " bytes");
        }

        [Test]
        public void Test_1_Supported_GDI_Codecs()
        {
            CodecUtils.ShowInfo();
        }

        [Test]
        public void Test_1b_ProbeVersions()
        {
            var candidates = ProbeReportBuilder.GetCandidates();
            foreach (var c in candidates)
            {
                Trace.WriteLine(ProbeReportBuilder.GetImplementationInfo(c));
            }
        }

        [Test]
        public void Test_2_Native_Report_Doesnt_Fail()
        {
            var bytes = ProbeReportBuilder.BuildRawProbe(SourceVideo);
            Assert.IsNotNull(bytes);
            Assert.Greater(bytes.Length, 0);
        }

        [Test]
        public void Test_3_Managed_Report_Doesnt_Fail()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);
            Assert.IsNotNull(report);
            if (report.HasVideo)
                Trace.WriteLine("Frame Rate is: " + report.Videos[0].FrameRate);
        }

        [Test]
        public void Test_4_Check_Video_Is_960x640()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            Assert.IsNotNull(report);
            Assert.IsNotNull(report.Videos);
            Assert.Greater(report.Videos.Count, 0);
            Assert.AreEqual(ExpectedWidth, report.Videos[0].Width);
            Assert.AreEqual(ExpectedHeight, report.Videos[0].Height);
        }

        [Test]
        public void Test_5_Has_Audio()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            Assert.IsNotNull(report);
            Assert.IsNotNull(report.Audios);
            Assert.Greater(report.Audios.Count, 0, "Video file has audio stream");
            Trace.WriteLine("Audio channels: " + report.Audios[0].Channels);
            Assert.Greater(report.Audios[0].Channels, 0, "Audio has channels info");
        }

        [Test]
        public void Test_5_Has_Audio_Using_Copy_of_AV_Report()
        {
            var report = AVProbeParser.Parse(Sample_AV_Report.Raw);

            Assert.IsNotNull(report);
            Assert.IsNotNull(report.Audios);
            Assert.Greater(report.Audios.Count, 0, "Video file has audio stream");
            Trace.WriteLine("Audio channels: " + report.Audios[0].Channels);
            Assert.Greater(report.Audios[0].Channels, 0, "Audio has channels info");
        }

        [Test]
        public void Test_5_Duration_Longer_7sec()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            Trace.WriteLine("Duration: " + report.Duration + " secs");
            Assert.IsTrue(report.Duration >= 7);
        }

        [Test]
        public void Test_6_Default_Screenshot_Size_Is_960x640()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            var g = new ScreenGrabber(report);
            using (var screen = g.Grab1Frame(5m))
            using (MemoryStream mem = new MemoryStream())
            {
                screen.CopyTo(mem);
                Trace.WriteLine("Default Screen as jpeg is " + mem.Length + " bytes");
                using (Bitmap bmp = new Bitmap(mem))
                {
                    Trace.WriteLine("Screen Size: " + bmp.Width + "*" + bmp.Height);
                    Assert.AreEqual(ExpectedWidth, bmp.Width);
                    Assert.AreEqual(ExpectedHeight, bmp.Height);
                }
            }
        }

        [Test]
        public void Test_7_Screenshot_Sized_By_Width()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            var g = new ScreenGrabber(report) { OutSize = ScreenGrabber.OutputSize.WidthIs(1000) };
            using (var screen = g.Grab1Frame(5m))
            using (MemoryStream mem = new MemoryStream())
            {
                screen.CopyTo(mem);
                Trace.WriteLine("Default Screen as jpeg is " + mem.Length + " bytes");
                using (Bitmap bmp = new Bitmap(mem))
                {
                    Trace.WriteLine("Screen Size: " + bmp.Width + "*" + bmp.Height);
                    Assert.AreEqual(1000, bmp.Width);
                }
            }
        }

        [Test]
        public void Test_8_Screenshot_Sized_By_Height()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            var g = new ScreenGrabber(report) { OutSize = ScreenGrabber.OutputSize.HeightIs(1000) };
            using (var screen = g.Grab1Frame(5m))
            using (MemoryStream mem = new MemoryStream())
            {
                screen.CopyTo(mem);
                Trace.WriteLine("Default Screen as jpeg is " + mem.Length + " bytes");
                using (Bitmap bmp = new Bitmap(mem))
                {
                    Trace.WriteLine("Screen Size: " + bmp.Width + "*" + bmp.Height);
                    Assert.AreEqual(1000, bmp.Height);
                }
            }
        }

        [Test]
        public void Test_9_Screenshot_Sized_By_Percentage()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            var g = new ScreenGrabber(report) { OutSize = ScreenGrabber.OutputSize.PercentsAre(50) };
            using (var screen = g.Grab1Frame(5m))
            using (MemoryStream mem = new MemoryStream())
            {
                screen.CopyTo(mem);
                Trace.WriteLine("Default Screen as jpeg is " + mem.Length + " bytes");
                using (Bitmap bmp = new Bitmap(mem))
                {
                    Trace.WriteLine("Screen Size: " + bmp.Width + "*" + bmp.Height);
                    Assert.AreEqual(ExpectedWidth / 2, bmp.Width);
                    Assert.AreEqual(ExpectedHeight / 2, bmp.Height);
                }
            }
        }

        [Test]
        public void Test_9a_Grab_Range()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            var g = new ScreenGrabber(report) { OutSize = ScreenGrabber.OutputSize.PercentsAre(50) };
            const int frames = 10;
            decimal firstFrame = 0.5m;
            using (var screens = g.GrabRange(firstFrame, frames, (frames - 1) / (report.Duration - firstFrame - 0.1m)))
            {
                Trace.WriteLine("ScreenGrabber.GrabRange(10 frames) returns " + screens.Count + " screenshot");
                foreach (var screen in screens)
                {
                    MemoryStream mem = new MemoryStream();
                    screen.CopyTo(mem);
                    using (Bitmap bmp = new Bitmap(mem))
                    {
                        Trace.WriteLine(string.Format("   Screenshot #{0,-2} is {1} bytes, {2}x{3} pixels",
                            screen.Index, mem.Length, bmp.Width, bmp.Height));

                        Assert.AreEqual(ExpectedWidth / 2, bmp.Width);
                        Assert.AreEqual(ExpectedHeight / 2, bmp.Height);
                    }
                }
            }
        }

        [Test, Category("ss")]
        public void Test_A1_Screenshot_Quality_Affects_Output_Size()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            Trace.WriteLine("Each next quality produces smaller or equal output image");
            int? prev = null;
            for (int quality = 2; quality <= 31; quality++)
            {
                var g = new ScreenGrabber(report)
                {
                    OutFormat = new ScreenGrabber.OutputFormat()
                    {
                        Encoder = ScreenGrabber.OutputEncoder.Jpeg, 
                        Quality = quality
                    }
                };

                Stopwatch sw = Stopwatch.StartNew();
                using (var screen = g.Grab1Frame(5m))
                using (MemoryStream mem = new MemoryStream())
                {
                    var msec = sw.ElapsedMilliseconds;
                    screen.CopyTo(mem);
                    Trace.WriteLine("Screen as jpeg [quality=" + quality + "] is " + mem.Length + " bytes, " + msec.ToString("n0") + " msecs");
                    int len = (int)mem.Length;
                    if (prev.HasValue)
                        Assert.LessOrEqual(len, prev.Value, string.Format("Size[quality={0}]=={1} should be larger then Size[quality={2}]=={3}",
                            quality - 1, prev, quality, len));

                    prev = len;
/*
                    using (Bitmap bmp = new Bitmap(mem))
                    {
                        Assert.AreEqual(ExpectedWidth, bmp.Width);
                        Assert.AreEqual(ExpectedHeight, bmp.Height);
                        Assert.AreEqual(CodecUtils.ImgFormat.Jpeg, CodecUtils.GetImageFormat(bmp.RawFormat));
                    }
*/
                }
            }

        }

        [Test]
        public void Test_A2_Screenshot_Using_All_Encodings()
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            var encoders = new[]
            {
                ScreenGrabber.OutputEncoder.Bmp, 
                ScreenGrabber.OutputEncoder.Gif, 
                ScreenGrabber.OutputEncoder.Jpeg,
                ScreenGrabber.OutputEncoder.Png,
            };

            foreach (var enc in encoders)
            {
                var outputFormat = new ScreenGrabber.OutputFormat() { Encoder = enc };
                var g = new ScreenGrabber(report) {OutFormat = outputFormat};
                Stopwatch sw = Stopwatch.StartNew();
                using(ScreenGrabber.Screen screen = g.Grab1Frame(5m))
                using (MemoryStream mem = new MemoryStream())
                {
                    var msec = sw.ElapsedMilliseconds;
                    screen.CopyTo(mem);
                    Trace.WriteLine("Screen size as " + enc + " is " + mem.Length + " bytes, " + msec.ToString("n0") + " msec");

                    using (Bitmap bmp = new Bitmap(mem))
                    {
                        Assert.AreEqual(ExpectedWidth, bmp.Width);
                        Assert.AreEqual(ExpectedHeight, bmp.Height);
                        var actual = CodecUtils.GetImageFormat(bmp.RawFormat).ToString();
                        var expected = enc.ToString();
                        Assert.AreEqual(
                            expected.ToLower().Replace("jpeg", "jpg"),
                            actual.ToLower().Replace("jpeg", "jpg"), 
                            "Expected encoder: " + enc);
                    }
                }
            }
        }

        [Test]
        public void Test_B2_Multithread_Reports_10seconds()
        {
            Multithread_Reports(TimeSpan.FromSeconds(10));
        }

        [Test, Explicit, Category("Stress")]
        public void Test_B2_Multithread_Reports_10minutes()
        {
            Multithread_Reports(TimeSpan.FromSeconds(600));
        }

        private static void Multithread_Reports(TimeSpan duration)
        {
            int total = 0;
            ParallelOptions po = new ParallelOptions() {MaxDegreeOfParallelism = 64};
            Stopwatch at = Stopwatch.StartNew();
            Dictionary<int, object> threads = new Dictionary<int, object>();
            Parallel.For(1, po.MaxDegreeOfParallelism, po, (index) =>
            {
                lock (threads) threads[Thread.CurrentThread.ManagedThreadId] = null;
                while (at.Elapsed <= duration)
                {
                    var report = ProbeReportBuilder.BuildProbe(SourceVideo);
                    Interlocked.Increment(ref total);
                }
            });

            Trace.WriteLine("Threads: " + threads.Count + ", Reports: " + total);
        }

        [Test]
        public void Test_B1_Multithread_Screenshot_10seconds()
        {
            MultiThread_Screenshot(10);
        }
        
        [Test, Explicit, Category("Stress")]
        public void Test_B1_Multithread_Screenshot_10minutes()
        {
            MultiThread_Screenshot(600);
        }

        private static void MultiThread_Screenshot(int duration)
        {
            var report = ProbeReportBuilder.BuildProbe(SourceVideo);

            int total = 0;
            ParallelOptions po = new ParallelOptions() {MaxDegreeOfParallelism = 64};
            Stopwatch at = Stopwatch.StartNew();
            Dictionary<int, object> threads = new Dictionary<int, object>();
            Parallel.For(1, po.MaxDegreeOfParallelism, po, (index) =>
            {
                while (at.Elapsed <= TimeSpan.FromSeconds(duration))
                {
                    try
                    {
                        var outputFormat = ScreenGrabber.OutputFormat.JpegMedium;
                        var g = new ScreenGrabber(report)
                        {
                            OutFormat = outputFormat,
                            OutSize = ScreenGrabber.OutputSize.PercentsAre(50)
                        };
                        using (var screen = g.Grab1Frame(5m))
                        using (MemoryStream mem = new MemoryStream())
                        {
                            screen.CopyTo(mem);
                        }

                        Interlocked.Increment(ref total);
                        lock (threads) threads[Thread.CurrentThread.ManagedThreadId] = null;
                    }
                    catch (Exception ex)
                    {
                        Trace.WriteLine("PARALLEL screenshot failed" + Environment.NewLine + ex);
                    }
                }
            });

            Trace.WriteLine("Threads: " + threads.Count + ", Screenshots: " + total);
        }

        // Source Video will be extracted on demand into file at a temp folder
        // And it will be deleted either on reboot or DomainUnload event
        private static string SourceVideo
        {
            get { return ResMan.VintageVideoAsFile.Value; }
        }

        private static Stopwatch StartAt = Stopwatch.StartNew();
        [TestFixtureSetUp]
        public void TestFixtureSetUp()
        {
            CrossInfo.AttachUnitTrace("MediaProbe Tests");
            StartAt = Stopwatch.StartNew();
        }

        Stopwatch TestAt = Stopwatch.StartNew();
        [SetUp]
        public void SetUp()
        {
            TestAt = Stopwatch.StartNew();
            CrossInfo.NextTest();
        }
        
        [TearDown]
        public void TearDown()
        {
            if (true || CrossInfo.ThePlatform == CrossInfo.Platform.MacOSX || CrossInfo.ThePlatform == CrossInfo.Platform.Linux)
            {
                var ws1 = (Process.GetCurrentProcess().WorkingSet64 / 1024L / 1024).ToString("n0");
                GC.WaitForPendingFinalizers();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                var ws2 = (Process.GetCurrentProcess().WorkingSet64 / 1024L / 1024).ToString("n0");
                Trace.WriteLine("***** " + TestAt.Elapsed + ", "
                    + "Working Set: " + ws1 + " Mb"
                    + (ws1 != ws2 ? ", after GC: " + ws2 + " Mb" : "")
                    + Environment.NewLine);
            }
            
        }


        [TestFixtureTearDown]
        public void TestFixtureTearDown()
        {
            AppDomain.CurrentDomain.DomainUnload += (sender, args) =>
            {
                if (File.Exists(SourceVideo))
                    File.Delete(SourceVideo);
            };

            Trace.WriteLine("Total time: " + StartAt.Elapsed);
        }
    }
}
